-
-
Notifications
You must be signed in to change notification settings - Fork 250
Bugfix – initialize&cache proper return value for generic, typed array. #1357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1357 |
&& (&method.return_value().type_tokens().to_string() == "VariantArray" | ||
|| method.is_generic()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should swap ||
operands, as is_generic
is cheaper to compute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side note – it might be good to check if we can set optimization level 1\2 for codegen or macros – it would allow compiler to optimize such things automatically and properly recognize no-ops 🤔.
for example – herestd::mem::take
will be no-op for -C opt-level=2
(handy link: https://rust.godbolt.org/) and almost no-op for opt-level 1 (don't ask me why almost). (black box and indirection because compiler was behaving silly and differently to normal usecases)
fn foobaz(somefoo: &mut SomeFoo) {
let mut vec = std::mem::take(&mut somefoo.foo);
for val in &mut vec {
*val += 1;
}
somefoo.foo = vec;
}
pub fn main(vec: Vec<u32>) {
std::hint::black_box({
let mut foo = SomeFoo {foo: vec, bar: 4};
foobaz(&mut foo)
});
}
let receiver = functions_common::make_receiver(method.qualifier(), ffi_arg_in); | ||
let object_ptr = &receiver.ffi_arg; | ||
|
||
let generic_params = method.return_value().generic_params(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let generic_params = method.return_value().generic_params(); | |
let maybe_generic_params = method.return_value().generic_params(); |
if it's often empty, that makes this a bit clearer...
// ExBuilder::new() constructor signature. | ||
let FnParamTokens { | ||
func_general_lifetime: simple_fn_lifetime, | ||
.. | ||
} = fns::make_params_exprs( | ||
required_fn_params.iter().cloned(), | ||
FnKind::ExBuilderConstructor, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is unrelated (the dead code you mentioned), right? Why exactly?
Could you extract it to its own commit so it's easier to isolate the changes in case of regressions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exactly?
func_general_lifetime
was never set, thus it was always empty – effectively this code was doing nothing.
fn is_generic(&self) -> bool { | ||
matches!(self.return_value().type_, Some(RustTy::GenericArray)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always empty lines between multi-line fn
decls
godot-codegen/src/models/domain.rs
Outdated
/// `Array<T>` | ||
/// | ||
/// Set by [`builtin_method_generic_ret`](crate::special_cases::builtin_method_generic_ret) | ||
GenericArray, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make clear that this is actually generic in the generated source, and T
is not just a placeholder here 🤔
|
||
/// Returns some generic type – such as `GenericArray` representing `Array<T>` – if method is marked as generic, `None` otherwise. | ||
/// | ||
/// Usually required to properly initialize the return value & cache its type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Usually required to properly initialize the return value & cache its type. | |
/// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357). |
match ( | ||
class_name.rust_ty.to_string().as_str(), | ||
method.name.as_str(), | ||
) { | ||
("Array", "duplicate") | ("Array", "slice") => Some(FnReturn { | ||
decl: RustTy::GenericArray.return_decl(), | ||
type_: Some(RustTy::GenericArray), | ||
}), | ||
_ => None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth dedicated constructor maybe? FnReturn::with_generic_builtin()
// SAFETY: We never write to the duplicated array, and all values read are read as `Variant`. | ||
let duplicate: VariantArray = unsafe { self.as_inner().duplicate(false) }; | ||
let duplicate: Self = unsafe { self.as_inner().duplicate(false) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Safety statement needs update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ended rewriting all the methods, since std:mem::transmute
was going from Array<T>
to Array<T>
, and cache was already initialized 😄
assert_eq!(script.get_global_name(), "CustomScriptForArrays".into()); | ||
} | ||
|
||
// Test that proper type has been set&cached while creating new Array. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For regression tests, add link to PR/issue, here e.g.
https://github.com/godot-rust/gdext/pull/1357
#[itest] | ||
fn array_inner_type() { | ||
let primary = Array::<Dictionary>::new(); | ||
let secondary = primary.duplicate_shallow(); | ||
assert_eq!(secondary.element_type(), primary.element_type()); | ||
let secondary = primary.duplicate_deep(); | ||
assert_eq!(secondary.element_type(), primary.element_type()); | ||
let subarray = primary.subarray_deep(.., None); | ||
assert_eq!(subarray.element_type(), primary.element_type()); | ||
let subarray = primary.subarray_shallow(.., None); | ||
assert_eq!(subarray.element_type(), primary.element_type()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, group subtests by introducing empty lines:
#[itest] | |
fn array_inner_type() { | |
let primary = Array::<Dictionary>::new(); | |
let secondary = primary.duplicate_shallow(); | |
assert_eq!(secondary.element_type(), primary.element_type()); | |
let secondary = primary.duplicate_deep(); | |
assert_eq!(secondary.element_type(), primary.element_type()); | |
let subarray = primary.subarray_deep(.., None); | |
assert_eq!(subarray.element_type(), primary.element_type()); | |
let subarray = primary.subarray_shallow(.., None); | |
assert_eq!(subarray.element_type(), primary.element_type()); | |
} | |
#[itest] | |
fn array_inner_type() { | |
let primary = Array::<Dictionary>::new(); | |
let secondary = primary.duplicate_shallow(); | |
assert_eq!(secondary.element_type(), primary.element_type()); | |
let secondary = primary.duplicate_deep(); | |
assert_eq!(secondary.element_type(), primary.element_type()); | |
let subarray = primary.subarray_deep(.., None); | |
assert_eq!(subarray.element_type(), primary.element_type()); | |
let subarray = primary.subarray_shallow(.., None); | |
assert_eq!(subarray.element_type(), primary.element_type()); | |
} |
Nice work, thank you! Note that this requires all generic functions to be unsafe, as they're only sound with one specific generic argument, and UB with all others. |
…ray runtime type. ----- Methods such as `duplicate_...` were initializing and caching typed `VariantArray` as a return value for outgoing calls. We fix it by initializing proper `Array<T>` in the first place.
…set. Remove `ExBuilderConstructor` – it was doing nothing, and in practice it was supposed to do the same thing as `ExBuilderConstructorLifetimed`.
d8577c9
to
aaffcb5
Compare
That's correct, I also created/moved separate safety docs for generated generic methods returning |
/// | ||
/// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array). | ||
}); | ||
if class_name.godot_ty == "Array" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite nice, but does anyone ever see these docs?
I guess the show up in IDE hints? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What Problem does this PR solve?
One mentioned in #1348. Long story short, we are responsible for initializing a return value from function; instead of initializing
Array<T>
we were initializingVariantArray
and cached its type as untyped. Oopsie.In other words, this code would panic:
This PR tweaks codegen, so instead of generating:
we generate:
I haven't been tweaking code too much (albeit I removed some dead code). I think it is OK and should hold for dictionaries as well, as long as nothing drastically changes.